package weka.classifiers.lazy;

import java.util.Enumeration;
import java.util.Vector;

import weka.classifiers.AbstractClassifier;
import weka.classifiers.Classifier;
import weka.core.DistanceFunction;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.Option;
import weka.core.OptionHandler;
import weka.core.SelectedTag;
import weka.core.Tag;
import weka.core.Utils;

public class LocalLearner extends IBk implements OptionHandler {

	/**
	 * 
	 */
	private static final long serialVersionUID = -8142441959770817059L;
	private Classifier localClassifier;
	private double kPercent = 0.10;

    public static final Tag[] TAGS_eval = {
    new Tag(0, "Basic"),
    new Tag(1, "k step-wise selected on training instances + test instance"),
    new Tag(2, "k step-wise selected on weighted training instances + test instance"),
    new Tag(3, "k step-wise selected on test instance"),
    new Tag(4, "k distance grouped selected on training instances + test instance"),
    new Tag(5, "k distance grouped selected on weighted training instances + test instance")
  };
    public int m_eval = 1;
	


	/**
	 * String describing default classifier.
	 */
	protected String defaultClassifierString() {

		return "weka.classifiers.trees.J48";
	}

	/**
	 * String describing default classifier.
	 */
	protected Classifier defaultClassifier() {

		return new weka.classifiers.trees.J48();
	}

	/**
	 * Generates the classifier.
	 * 
	 * @param instances
	 *            set of instances serving as training data
	 * @throws Exception
	 *             if the classifier has not been generated successfully
	 */
	public void buildClassifier(Instances instances) throws Exception {

		// can classifier handle the data?
		getCapabilities().testWithFail(instances);

		// remove instances with missing class
		instances = new Instances(instances);
		instances.deleteWithMissingClass();

		m_NumClasses = instances.numClasses();
		m_ClassType = instances.classAttribute().type();
		m_Train = new Instances(instances, 0, instances.numInstances());

		// Throw away initial instances until within the specified window size
		if ((m_WindowSize > 0) && (instances.numInstances() > m_WindowSize)) {
			m_Train = new Instances(m_Train, m_Train.numInstances()
					- m_WindowSize, m_WindowSize);
		}

		m_NumAttributesUsed = 0.0;
		for (int i = 0; i < m_Train.numAttributes(); i++) {
			if ((i != m_Train.classIndex())
					&& (m_Train.attribute(i).isNominal() || m_Train
							.attribute(i).isNumeric())) {
				m_NumAttributesUsed += 1.0;
			}
		}
		
		if (kPercent == 1) {
			getClassifier().buildClassifier(m_Train);
		}

		m_NNSearch.setInstances(m_Train);

		// Invalidate any currently cross-validation selected k
		m_kNNValid = false;
		
		m_kNN = (int) (m_Train.numInstances()*kPercent);
		if (m_kNN == 0)
			m_kNN = 1;
		//m_kNNUpper = m_kNN;
		
	}

	/**
	 * Calculates the class membership probabilities for the given test
	 * instance.
	 * 
	 * @param instance
	 *            the instance to be classified
	 * @return predicted class probability distribution
	 * @throws Exception
	 *             if an error occurred during the prediction
	 */
	public double[] distributionForInstance(Instance instance) throws Exception {
		
		if (kPercent == 1) {
			return getClassifier().distributionForInstance(instance);
		}
		
		if (m_Train.numInstances() == 0) {
			throw new Exception("No training instances!");
		}
		if ((m_WindowSize > 0) && (m_Train.numInstances() > m_WindowSize)) {
			m_kNNValid = false;
			boolean deletedInstance = false;
			while (m_Train.numInstances() > m_WindowSize) {
				m_Train.delete(0);
			}
			// rebuild datastructure KDTree currently can't delete
			if (deletedInstance == true)
				m_NNSearch.setInstances(m_Train);
		}

		// Select k by cross validation
		if (!m_kNNValid && (m_CrossValidate) && (m_kNNUpper >= 1)) {
			crossValidate();
		}

		m_NNSearch.addInstanceInfo(instance);

		Instances neighbours;
		switch (m_eval) {
		case 0:
			neighbours = m_NNSearch.kNearestNeighbours(instance, m_kNN);
			try {
				getClassifier().buildClassifier(neighbours);
				return getClassifier().distributionForInstance(instance);
			} catch (IllegalArgumentException e) {
				double[] distances = m_NNSearch.getDistances();
				return makeDistribution(neighbours, distances);
			}
		case 1: case 2: case 3: case 4: case 5:
			int maxK = 0;
			switch (m_eval) {
			case 1:
				maxK = useAllInstances(instance);
				break;
			case 2:
				maxK = useWeightedInstances(instance);
				break;
			case 3:
				maxK = useTestInstance(instance);
				break;
			case 4:
				maxK = useOnlyTransitions(instance);
				break;
			case 5:
				maxK = useWeightedTransitions(instance);
				break;
			}
			
			neighbours = m_NNSearch.kNearestNeighbours(instance, maxK);
			try {
				getClassifier().buildClassifier(neighbours);
				return getClassifier().distributionForInstance(instance);
			} catch (IllegalArgumentException e) {
				double[] distances = m_NNSearch.getDistances();
				return makeDistribution(neighbours, distances);
			}
		}
		throw new Exception("LocalLearner:distributionForInstance: Impossible point to reach");
	}
	
	private int useWeightedTransitions(Instance instance) throws Exception {

		double maxPoint = Double.MIN_VALUE;
		int maxK = 0;
		Instances allNeighbours = m_NNSearch.kNearestNeighbours(instance, m_kNN);
		Vector<Integer> transitions = new Vector<Integer>();
		double[] distances = m_NNSearch.getDistances();
		double d = distances[0];
		for (int i = 1; i < distances.length; i++) {
			if (d != distances[i]) {
				transitions.add(i);
				d = distances[i];
			}					
		}
		DistanceFunction df = m_NNSearch.getDistanceFunction();
		transitions.add(distances.length);
		for (int n1 = 0; n1 < transitions.size(); n1++) {
			int k = transitions.get(n1);
			getClassifier().buildClassifier(m_NNSearch.kNearestNeighbours(instance, k));
			double[] distribution = getClassifier().distributionForInstance(instance);
			double sum = 0;
			for (int i = 0; i < k; i++) {
				double[] dist = getClassifier().distributionForInstance(allNeighbours.get(i));
				for (int j = 0; j < instance.numClasses(); j++) {
					double distance = df.distance(instance, allNeighbours.get(i));
					distribution[j] += dist[j] * (1 - distance);//s[i]);
					sum += dist[j] * (1 - distance);//s[i]);
				}
			}
			for (int j = 0; j < instance.numClasses(); j++)
				distribution[j] /= sum;
			double points = selectK(distribution, k);
			if (maxPoint < points) {
				maxK = k;
				maxPoint = points;			
			}				
		}
		return maxK;
	}
	
	private int useOnlyTransitions(Instance instance) throws Exception {

		double maxPoint = Double.MIN_VALUE;
		int maxK = 0;
		Instances allNeighbours = m_NNSearch.kNearestNeighbours(instance, m_kNN);
		Vector<Integer> transitions = new Vector<Integer>();
		double[] distances = m_NNSearch.getDistances();
		double d = distances[0];
		for (int i = 1; i < distances.length; i++) {
			if (d != distances[i]) {
				transitions.add(i);
				d = distances[i];
			}					
		}
		transitions.add(distances.length);
		for (int n1 = 0; n1 < transitions.size(); n1++) {
			int k = transitions.get(n1);
			getClassifier().buildClassifier(m_NNSearch.kNearestNeighbours(instance, k));
			double[] distribution = getClassifier().distributionForInstance(instance);
			double sum = 0;
			for (int i = 0; i < k; i++) {
				double[] dist = getClassifier().distributionForInstance(allNeighbours.get(i));
				for (int j = 0; j < instance.numClasses(); j++) {
					distribution[j] += dist[j];
					sum += dist[j];
				}
			}
			for (int j = 0; j < instance.numClasses(); j++)
				distribution[j] /= sum;
			double points = selectK(distribution, k);
			if (maxPoint < points) {
				maxK = k;
				maxPoint = points;			
			}				
		}
		return maxK;
	}
	
	private int useWeightedInstances(Instance instance) throws Exception {
		double maxPoint = Double.MIN_VALUE;
		int maxK = 0;
		for (int k = instance.numClasses()+1; k <= m_kNN; k++) {
			Instances neighbours = m_NNSearch.kNearestNeighbours(instance, k);
			DistanceFunction df = m_NNSearch.getDistanceFunction();
			
			getClassifier().buildClassifier(neighbours);
			double[] distribution = getClassifier().distributionForInstance(instance);
			double sum = 0;
			for (int i = 0; i < k; i++) {
				double[] dist = getClassifier().distributionForInstance(neighbours.get(i));
				for (int j = 0; j < instance.numClasses(); j++) {
					double distance = df.distance(instance, neighbours.get(i));
					distribution[j] += dist[j] * (1 - distance);//s[i]);
					sum += dist[j] * (1 - distance);//s[i]);
				}
			}
			for (int j = 0; j < instance.numClasses(); j++)
				distribution[j] /= sum;
			double points = selectK(distribution, k);
			if (maxPoint < points) {
				maxK = k;
				maxPoint = points;			
			}				
		}
		return maxK;
	}
	
	private int useTestInstance(Instance instance) throws Exception {
		double maxPoint = Double.MIN_VALUE;
		int maxK = 0;
		for (int k = instance.numClasses()+1; k <= m_kNN; k++) {
			Instances neighbours = m_NNSearch.kNearestNeighbours(instance, k);
			
			getClassifier().buildClassifier(neighbours);
			double[] distribution = getClassifier().distributionForInstance(instance);
			double points = selectK(distribution, k);
			if (maxPoint < points) {
				maxK = k;
				maxPoint = points;			
			}				
		}
		return maxK;
	}
	
	private int useAllInstances(Instance instance) throws Exception {
		double maxPoint = Double.MIN_VALUE;
		int maxK = 0;
		for (int k = instance.numClasses()+1; k <= m_kNN; k++) {
			Instances neighbours = m_NNSearch.kNearestNeighbours(instance, k);
			
			getClassifier().buildClassifier(neighbours);
			double[] distribution = getClassifier().distributionForInstance(instance);
			double sum = 0;
			for (int i = 0; i < k; i++) {
				double[] dist = getClassifier().distributionForInstance(neighbours.get(i));
				for (int j = 0; j < instance.numClasses(); j++) {
					distribution[j] += dist[j];
					sum += dist[j];
				}
			}
			for (int j = 0; j < instance.numClasses(); j++)
				distribution[j] /= sum;
			double points = selectK(distribution, k);
			if (maxPoint < points) {
				maxK = k;
				maxPoint = points;			
			}				
		}
		return maxK;
	}
	
	private double selectK(double[] distribution, int k) {
		double sum = 0;
		//Vad händer här?
		for (int j = 0; j < distribution.length; j++) {
			double f = distribution[(int) j] * k;
			distribution[j] = (f + 1) / (k + distribution.length);
			sum += distribution[j];
		}
		for (int j = 0; j < distribution.length; j++)
			distribution[j] /= sum;
		sum = 0;
		double max = 0;
		for (int i = 0; i < distribution.length; i++) {
			if (distribution[i] > max) {
				max = distribution[i];
			} 
			sum += distribution[i];
		}
		return max/sum;
	}

	/**
	 * Returns an enumeration describing the available options
	 * 
	 * @return an enumeration of all the available options
	 */
	public Enumeration<Option> listOptions() {

		Vector<Option> newVector = new Vector<Option>();

		newVector.addElement(new Option(
				"\tFull name of extraction classifier.\n" + "\t(default: "
						+ defaultClassifierString() + ")", "E", 1,
				"-E"));

		newVector.addElement(new Option("", "", 0,
				"\nOptions specific to classifier "
						+ localClassifier.getClass().getName() + ":"));
		Enumeration<Option> enu = ((OptionHandler) localClassifier)
				.listOptions();
		while (enu.hasMoreElements()) {
			newVector.addElement(enu.nextElement());
		}

		newVector
				.addElement(new Option(
						"\tPercent of nearest neighbours (k) used in classification.\n"
								+ "\t(Default = 0.1)", "P", 1,
						"-P <percent of neighbors>"));

		newVector
				.addElement(new Option(
						"\tDeterminesif and how a local search for an optimal k is performed.\n"
								+ "\t(Default Basic)", "S", 1,
						"-S"));

		Enumeration<Option> en = super.listOptions();
		while (en.hasMoreElements()) {
			Option tmp = en.nextElement();
			newVector.addElement(tmp);
		}

		return newVector.elements();
	}

	/**
	 * Parses a given list of options. Valid options are:
	 * <p>
	 * 
	 * -B classifierstring <br>
	 * Classifierstring should contain the full class name of a scheme included
	 * for selection followed by options to the classifier (required, option
	 * should be used once for each classifier).
	 * <p>
	 * 
	 * @param options
	 *            the list of options as an array of strings
	 * @exception Exception
	 *                if an option is not supported
	 */
	public void setOptions(String[] options) throws Exception {
		String classifierString = Utils.getOption('E', options);
		String[] classifierSpec = Utils.splitOptions(classifierString);
		String classifierName;
		if (classifierSpec.length == 0) {
			classifierName = defaultClassifierString();
		} else {
			classifierName = classifierSpec[0];
			classifierSpec[0] = "";
		}
		localClassifier = AbstractClassifier.forName(classifierName,
				classifierSpec);
		
		String knnString = Utils.getOption('P', options);
		if (knnString.length() != 0) {
			setKPercent(Double.parseDouble(knnString));
		} else {
			setKPercent(0.10);
		}
        
		String tmp = Utils.getOption("S", options);
        if (tmp.length() != 0)
            setEvaluation (
                    new SelectedTag (Integer.parseInt (tmp), TAGS_eval));
        else
            setEvaluation (
                    new SelectedTag (m_eval, TAGS_eval));

		super.setOptions(options);
	}

	/**
	 * Gets the current settings of the Classifier.
	 * 
	 * @return an array of strings suitable for passing to setOptions
	 */
	public String[] getOptions() {

		String[] options;
		Vector<String> result = new Vector<String>();

		result.add("-E");
		result.add(getClassifier().getClass().getName()
				+ " "
				+ Utils.joinOptions(((OptionHandler) getClassifier())
						.getOptions()));
		result.add("-P");
		result.add("" + getKPercent());
		
		result.add("-S");
		result.add("" + m_eval);

		options = super.getOptions();
		for (int i = 0; i < options.length; i++) {
			result.add(options[i]);
		}

		String[] array = new String[result.size()];
		for (int i = 0; i < result.size(); i++)
			array[i] = result.elementAt(i).toString();

		return array; // (String[]) result.toArray(new String[result.size()]);
	}
	
	/**
     * Returns the tip text for this property
     * @return tip text for this property suitable for
     * displaying in the explorer/experimenter gui
     */
    public String evaluationTipText() {
        return
                "...";
    }

    /**
     * Get the value of crt.
     *
     * @return Value of crt.
     */
    public SelectedTag getEvaluation() {

        return new SelectedTag(m_eval, TAGS_eval);
    }

    /**
     * Set the value of crt.
     *
     * @param creationType Value to assign to crt.
     */
    public void setEvaluation(SelectedTag value) {
        if (value.getTags() == TAGS_eval)
            m_eval = value.getSelectedTag().getID();
    }

	/**
	 * Returns the tip text for this property.
	 * 
	 * @return tip text for this property suitable for displaying in the
	 *         explorer/experimenter gui
	 */
	public String KPercentTipText() {
		return "The percent of neighbours to use.";
	}

	/**
	 * Set the percent of neighbours the learner is to use.
	 * 
	 * @param k
	 *            the percent of neighbours.
	 */
	public void setKPercent(double k) {
		kPercent = k;
	}

	/**
	 * Gets the percent of neighbours the learner will use.
	 * 
	 * @return the percent of neighbours.
	 */
	public double getKPercent() {

		return kPercent;
	}

	/**
	 * Returns the tip text for this property
	 * 
	 * @return tip text for this property suitable for displaying in the
	 *         explorer/experimenter gui
	 */
	public String ClassifierTipText() {
		return "The rule extraction classifier to be used.";
	}

	/**
	 * Sets the rule extraction classifer.
	 * 
	 * @param classifier
	 *            a rule extraction classifier with all options set.
	 */
	public void setClassifier(Classifier classifier) {

		this.localClassifier = classifier;
	}

	/**
	 * Gets the rule extraction classifer.
	 * 
	 * @return the Rule Extraction Classifier
	 */
	public Classifier getClassifier() {
		if (localClassifier == null)
			localClassifier = defaultClassifier();
		
		return localClassifier;
	}

}
